// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2023. All rights reserved.
 * Author: Huawei OS Kernel Lab
 * Create: Fri Sep 23 10:45:12 2022
 */

#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/mount.h>
#include <linux/module.h>
#include <linux/file.h>
#include <linux/pid_namespace.h>
#include <liblinux/kernfs.h>
#include <uapi/linux/mount.h>
#include <linux/init_syscalls.h>

#include "../../../fs/proc/internal.h"

#include <trace/hooks/liblinux.h>

struct file *alloc_file(const struct path *path, int flags,
		const struct file_operations *fop);

static struct vfsmount *proc_mnt;

static int __liblinux_procfs_open(struct inode *inode, int flags, struct file **file)
{
	struct dentry dentry;
	struct proc_dir_entry *dp = NULL;
	struct file *f = NULL;
	int ret;

	struct path path = {
		.mnt = NULL,
		.dentry = &dentry,
	};
	dentry.d_inode = inode;

	dp = PROC_I(inode)->pde;
	if ((dp == NULL) || (dp->proc_ops == NULL))
		return -EINVAL;

	f = alloc_file(&path, flags, fops_get(inode->i_fop));
	if (IS_ERR(f))
		return PTR_ERR(f);

	if (f->f_op->open != NULL) {
		ret = f->f_op->open(inode, f);
		if (ret < 0) {
			put_filp(f);
			return ret;
		}
	}

	f->f_path.dentry = NULL;
	*file = f;
	return 0;
}

static int __liblinux_procfs_rw(unsigned long long pos, unsigned long ubuf,
				unsigned long size, void *ctx,
				unsigned long *rw_size /* out */, bool do_write)
{
	struct inode *inode = NULL;
	struct file *filep = NULL;
	int ret;
	loff_t pos_loff;

	if ((ctx == NULL) || (pos > (unsigned long long)LLONG_MAX))
		return -EINVAL;

	pos_loff = (loff_t)pos;
	inode = (struct inode *)ctx;
	ret = __liblinux_procfs_open(inode, (do_write ? O_WRONLY : O_RDONLY), &filep);
	if (ret < 0)
		return ret;

	if (do_write)
		ret = vfs_write_full(filep, (const char __user *)ubuf, size, &pos_loff);
	else
		ret = vfs_read_full(filep, (char __user *)ubuf, size, &pos_loff);

	if (ret >= 0) {
		*rw_size = ret;
		ret = 0;
	}

	fput(filep);
	return ret;
}

static int __liblinux_procfs_read(unsigned long long pos, unsigned long ubuf,
				  unsigned long size, void *ctx,
				  unsigned long *rsize /* out */)
{
	return __liblinux_procfs_rw(pos, ubuf, size, ctx, rsize, false);
}

static int __liblinux_procfs_write(unsigned long long pos, unsigned long ubuf,
				   unsigned long size, void *ctx,
				   unsigned long *wsize /* out */)
{
	return __liblinux_procfs_rw(pos, ubuf, size, ctx, wsize, true);
}

static struct liblinux_pal_kernfs_ops liblinux_procfs_ops = {
	.read = __liblinux_procfs_read,
	.write = __liblinux_procfs_write,
};

static char *__liblinux_acquire_procfs_path(struct proc_dir_entry *dp,
					    char *buf, size_t buflen)
{
	char *p = &buf[buflen - 1];

	if (dp == NULL)
		return NULL;

	*--p = '\0';

	do {
		if (p - buf < dp->namelen + 1) {
			buf[0] = '\0';
			p = NULL;
			break;
		}

		p -= dp->namelen;
		memcpy(p, dp->name, dp->namelen);
		*--p = '/';
		dp = dp->parent;
	} while ((dp != NULL) && (strcmp(dp->name, "/proc")));

	return p;
}

static int init_proc_mount(void)
{
	struct file_system_type *proc_fs_type;
	void *ptr_ret;

	proc_fs_type = get_fs_type("proc");
	if (!proc_fs_type) {
		pr_err("Failed to find procfs\n");
		return -ENODEV;
	}
	ptr_ret = kern_mount(proc_fs_type);
	put_filesystem(proc_fs_type);
	if (IS_ERR(ptr_ret)) {
		pr_err("Failed to mount procfs\n");
		return PTR_ERR(ptr_ret);
	}
	proc_mnt = ptr_ret;
	return 0;
}

void liblinux_proc_register(struct proc_dir_entry *dp)
{
	char *buf = NULL;
	char *pathname = NULL;
	char *linkname = dp->data;
	int ret;

	if (!proc_mnt)
		if (init_proc_mount() < 0)
			return;

	buf = kzalloc(PATH_MAX, GFP_KERNEL);
	if (buf == NULL) {
		pr_err("liblinux: proc kzalloc buf failed\n");
		return;
	}

	pathname = __liblinux_acquire_procfs_path(dp, buf, PATH_MAX);
	if (pathname == NULL) {
		pr_err("liblinux:ientry '%s/%s' m=0x%x\n", dp->parent->name, dp->name, dp->mode);
		goto out;
	}

	/* skip head '/' */
	pathname++;

	if (S_ISREG(dp->mode)) {
		struct inode *inode = NULL;

		pde_get(dp);
		inode = proc_get_inode(proc_mnt->mnt_sb, dp);
		if (inode == NULL) {
			ret = -ENOMEM;
		} else {
			ret = liblinux_pal_procfs_create_file(pathname, &liblinux_procfs_ops,
					(void *)inode, (unsigned int)dp->mode);
			if (ret < 0)
				iput(inode);
		}
	} else if (S_ISDIR(dp->mode)) {
		ret = liblinux_pal_procfs_create_dir(pathname, (unsigned int)dp->mode);
	} else if (S_ISLNK(dp->mode)) {
		if (linkname == NULL)
			ret = -EINVAL;
		else
			ret = liblinux_pal_procfs_create_link(pathname,
							(unsigned int)dp->mode, linkname);
	} else {
		pr_warn("liblinux proreg '%s/%s' m=0x%x\n", dp->parent->name, dp->name, dp->mode);
		ret = 0;
	}

	if (ret < 0)
		pr_err("liblinux: create proc entry '%s/%s' failed, mode=0x%x, ret=%d\n",
			 dp->parent->name, dp->name, dp->mode, ret);

out:
	kfree(buf);
}

/* vendor hook for procfs */
static DEFINE_MUTEX(path_lock);
static void hook_proc_register(void *__data, int action, struct proc_dir_entry *dp)
{
	switch (action) {
	case VH_REG_PROCFS_PREPARE:
		pde_get(dp);
		break;
	case VH_REG_PROCFS:
		mutex_lock(&path_lock);
		liblinux_proc_register(dp);
		mutex_unlock(&path_lock);
		pde_put(dp);
		break;
	}
}
INIT_VENDOR_HOOK(ldk_vh_proc_register, hook_proc_register);

void liblinux_procfs_unlink(struct proc_dir_entry *dp)
{
	char *buf = NULL;
	char *pathname = NULL;

	buf = kzalloc(PATH_MAX, GFP_KERNEL);
	if (buf == NULL) {
		pr_err("liblinux: proc kzalloc buf failed\n");
		return;
	}

	pathname = __liblinux_acquire_procfs_path(dp, buf, PATH_MAX);
	if (pathname == NULL) {
		pr_err("liblinux: invalid proc entry '%s/%s', mode=0x%x\n",
		dp->parent->name, dp->name, dp->mode);
		goto out;
	}

	/* skip head '/' */
	pathname++;

	(void)liblinux_pal_procfs_delete(pathname);
out:
	kfree(buf);
}

static void hook_remove_proc_entry(void *__data, struct proc_dir_entry *de)
{
	if (de)
		liblinux_procfs_unlink(de);
}
INIT_VENDOR_HOOK(ldk_vh_remove_proc_entry, hook_remove_proc_entry);

static void hook_remove_proc_subtree(void *__data, struct proc_dir_entry *de)
{
	liblinux_procfs_unlink(de);
}
INIT_VENDOR_HOOK(ldk_vh_remove_proc_subtree, hook_remove_proc_subtree);

static int __init ldk_auto_mount_proc(void)
{
	int ret;
	const char *mp_dir = "/proc";

	ret = init_mkdir(mp_dir, 0755);
	if (ret && ret != -EEXIST) {
		pr_err("mkdir `%s` failed, err=%d\n", mp_dir, ret);
		return ret;
	}
	ret = init_mount("procfs", mp_dir, "proc", MS_SILENT, NULL);
	if (ret) {
		pr_err("auto mount procfs failed, err=%d\n", ret);
		return ret;
	}
	pr_debug("auto mount proc fs success\n");
	return 0;
}
late_initcall(ldk_auto_mount_proc)
